Java的long、double类型的原子性读取问题

In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn’t happen at all. No side effects of an atomic action are visible until the action is complete.

以上是关于原子性的操作的相关描述。

在Java中,以下的操作可以认为是原子操作

  1. 对于引用变量、大多数的原始类型变量的读、写(所有的类型除了longdouble)都是原子性的(这个只有才32位的JVM成立)
  2. 所有申明为volatile的变量(包括longdouble变量)的读、写都是原子性的

在Java中,自增、自减操作都不是原子性操作,很容易理解。但是longduoble的读写却不是原子性的问题,却不太好理解。

检验

在32位JVM上,通过两个线程对同一个成员变量进行读写,来测试longdouble类型变量的读写不是原子性操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

public class UnatomicLongDemo implements Runnable {

private static long test = 0;

private final long val;

public UnatomicLongDemo(long val) {
this.val = val;
}

@Override
public void run() {
while (!Thread.interrupted()) {
test = val;//两个线程同时断写test变量,如果test变量的读写操作是原子性的,那么test只能是-1或者0
}
}

public static void main(String[] args) {
Thread t1 = new Thread(new UnatomicLongDemo(-1));
Thread t2 = new Thread(new UnatomicLongDemo(0));

System.out.println(Long.toBinaryString(-1));
System.out.println(pad(Long.toBinaryString(0), 64));

t1.start();
t2.start();

long switchVal;
while ((switchVal = test) == -1 || switchVal == 0) {
//如果test、switchVal的操作是原子性的,那么就应该是死循环,否则就会跳出该循环
System.out.println("testing...");
}

System.out.println(pad(Long.toBinaryString(switchVal), 64));
System.out.println(switchVal);

t1.interrupt();
t2.interrupt();
}

//将0补齐到固定长度
private static String pad(String s, int targetLength) {
int n = targetLength - s.length();
for (int x = 0; x < n; x++) {
s = "0" + s;
}
return s;
}

}

测试结果为

这里写图片描述

通过这个例子可以看出

  1. 在32位JVM上,对long型变量test的读取或者对long型变量switchVal的写入不是原子性的。

  2. 非原子性的读、写只是造成longdouble类型变量的高、低32位数据不一致

这是由于在32位JVM中对64位的数据的读、写分两步,每一步读或者写32位的数据,这样就会造成两个线程对同一个变量的读写出现一个线程写高32位、另一个线程写入低32位数据。这样此变量的数据就出现不一致的情况。这时候volatile关键字可以防止这种现象发生,因为java的内存模型保证了valotile修饰的longdouble变量的读写是原子性的。